/**
 * Copyright 2009 Genius-Field All Rights Reserved.
 */
package com.geniusfield.sql;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Properties;
import java.util.Timer;
import java.util.TimerTask;

import com.geniusfield.GFException;

/**
 * The DBConnectionPool class is used to manage the connection pool. For now, it
 * only supports JDK 1.6 and JDBC 5.1 for MySQL. We may make it be compatible
 * with other versions of JDKs and JDBCs.<BR>
 * Use {@link #getInstance()} to get a unique instance or use
 * {@link #getInstance(File)} to specify the properties file path.<BR>
 * Use {@link #getConn()} to get a connection. You may use it just as usual.
 * After using the connection, always use {@link Conn#close()} to free it.<BR>
 * There're two scheduled tasks to maintain good health of the connection pool.
 * It will check the connection validity and release some waste connections.
 * 
 * @author Junjie Jin
 * 
 */
public class DBConnectionPool {
    private static boolean initialized = false;
    private static File file;
    private static Properties dbProperties;
    private boolean customized;
    private String url, port, protocol, classname, dbname, username, password,
            connurl;
    private int minConn, maxConn, totalConnCount = 0;
    private long timeout, autoReleaseTime, checkValidTime;
    private LinkedList<Conn> availableConn = new LinkedList<Conn>();
    private HashSet<Conn> usedConn = new HashSet<Conn>();
    private Timer freeCheck, validCheck;

    private static class Instance {
        private static DBConnectionPool instance = new DBConnectionPool();
    }

    /**
     * @return a unique instance of DBConnectionPool
     * @throws GFException
     */
    public static DBConnectionPool getInstance() throws GFException {
        if (initialized) {
            return Instance.instance;
        } else {
            if (DBConnectionPool.file == null) {
                DBConnectionPool.file = new File("db.properties");
            }
            DBConnectionPool instance = Instance.instance;
            if (initialized) {
                return instance;
            } else {
                throw new GFException("Instance cannot be initialized");
            }
        }
    }

    /**
     * @param file
     *            the file containing configuration parameters
     * @return a unique instance of DBConnectionPool
     * @throws GFException
     */
    public static DBConnectionPool getInstance(File file) throws GFException {
        if (initialized) {
            return Instance.instance;
        } else {
            DBConnectionPool.file = file;
            return DBConnectionPool.getInstance();
        }
    }

    /**
     * @param dbProperties
     *            the properties containing configuration parameters
     * @return a unique instance of DBConnectionPool
     * @throws GFException
     */
    public static DBConnectionPool getInstance(Properties dbProperties)
            throws GFException {
        if (initialized) {
            return Instance.instance;
        } else {
            DBConnectionPool.dbProperties = dbProperties;
            DBConnectionPool instance = Instance.instance;
            if (initialized) {
                return instance;
            } else {
                throw new GFException("Instance cannot be initialized");
            }
        }
    }

    private DBConnectionPool() {
        try {
            if (DBConnectionPool.dbProperties == null) {
                FileInputStream fis = new FileInputStream(DBConnectionPool.file);
                DBConnectionPool.dbProperties = new Properties();
                DBConnectionPool.dbProperties.load(fis);
                getProperties(DBConnectionPool.dbProperties);
                fis.close();
            } else {
                getProperties(DBConnectionPool.dbProperties);
            }
            if (!customized) {
                connurl = "jdbc:" + protocol + "://" + url + ":" + port + "/"
                        + dbname + "?characterEncoding=utf8";
            }
            Class.forName(classname);
            fillPool();
            freeCheck = new Timer(true);
            validCheck = new Timer(true);
            freeCheck.schedule(new FreeCheck(), autoReleaseTime,
                    autoReleaseTime);
            validCheck.schedule(new ValidCheck(), checkValidTime,
                    checkValidTime);
            DBConnectionPool.initialized = true;
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void getProperties(Properties dbProperties) {
        customized = Boolean.parseBoolean(dbProperties.getProperty(
                "customized", "false").trim());
        connurl = dbProperties.getProperty("connurl",
                "jdbc:mysql://127.0.0.1:3306/test?characterEncoding=utf8")
                .trim();
        url = dbProperties.getProperty("url", "127.0.0.1").trim();
        port = dbProperties.getProperty("port", "3306").trim();
        protocol = dbProperties.getProperty("protocol", "mysql").trim()
                .toLowerCase(Locale.ENGLISH);
        classname = dbProperties.getProperty("classname",
                "com.mysql.jdbc.Driver").trim();
        dbname = dbProperties.getProperty("dbname", "test").trim();
        username = dbProperties.getProperty("username", "root");
        password = dbProperties.getProperty("password", "");
        minConn = Integer.parseInt(dbProperties.getProperty("minConn", "10")
                .trim());
        maxConn = Integer.parseInt(dbProperties.getProperty("maxConn", "20")
                .trim());
        timeout = Long.parseLong(dbProperties.getProperty("timeout", "100")
                .trim());
        autoReleaseTime = Long.parseLong(dbProperties.getProperty(
                "autoReleaseTime", "300000").trim());
        checkValidTime = Long.parseLong(dbProperties.getProperty(
                "checkValidTime", "14400000").trim());
    }

    private Conn createConn() throws SQLException {
        Connection con = DriverManager.getConnection(connurl, username,
                password);
        return new Conn(this, con);
    }

    private synchronized void fillPool() throws SQLException {
        while (totalConnCount < minConn) {
            availableConn.addLast(createConn());
            totalConnCount++;
        }
    }

    /**
     * @return a connection to the URL
     * @throws SQLException
     * @throws GFException
     */
    public synchronized Conn getConn() throws SQLException, GFException {
        Conn conn;
        long startTime = new Date().getTime();
        while (true) {
            if (!availableConn.isEmpty()) {
                conn = availableConn.removeFirst();
                conn.setLastUseTime(startTime);
                usedConn.add(conn);
                return conn;
            } else if (totalConnCount < maxConn) {
                conn = createConn();
                conn.setLastUseTime(startTime);
                totalConnCount++;
                usedConn.add(conn);
                return conn;
            } else {
                try {
                    wait(timeout);
                    if (new Date().getTime() - startTime >= timeout) {
                        throw new GFException("Getting connection timeout");
                    } else {
                        continue;
                    }
                } catch (InterruptedException e) {
                    throw new GFException("The waiting thread is interrupted");
                }
            }
        }
    }

    synchronized void freeConn(Conn conn) {
        if (usedConn.remove(conn)) {
            availableConn.addLast(conn);
            notify();
        }
    }

    /**
     * @return the number of available connections in the pool
     */
    public int getAvailableConnCount() {
        return availableConn.size();
    }

    /**
     * @return the number of all the connections generated by the pool
     */
    public int getTotalConnCount() {
        return totalConnCount;
    }

    private class FreeCheck extends TimerTask {
        public void run() {
            Conn conn;
            long now = new Date().getTime();
            synchronized (DBConnectionPool.this) {
                for (Iterator<Conn> i = usedConn.iterator(); i.hasNext();) {
                    conn = i.next();
                    if (now - conn.getLastUseTime() > autoReleaseTime) {
                        i.remove();
                        availableConn.add(conn);
                        DBConnectionPool.this.notify();
                    }
                }
            }
        }
    }

    private class ValidCheck extends TimerTask {
        public void run() {
            Conn conn;
            synchronized (DBConnectionPool.this) {
                for (Iterator<Conn> i = availableConn.iterator(); i.hasNext();) {
                    conn = i.next();
                    try {
                        if (!conn.isValid(1)) {
                            i.remove();
                            totalConnCount--;
                        }
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                }
                try {
                    fillPool();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
